<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>

</body>
<script src="../../../jquery-1.12.4.min.js"></script>
<script>
    //【1】一句话介绍 call：
    // call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
    var foo = {
        value: 1
    };

    function bar() {
        console.log(this.value);
    }
    bar.call(foo); // 1
    // 注意两点：
    // 1. call 改变了 this 的指向， 指向到 foo
    // 2. bar 函数执行了

    // 【2】模拟实现第一步（思路分析）
    //（1）将调用方法作为属性，添加到调用主体上；然后将该方法删除
    var foo = {
        value: 1,
        bar: function() {
            console.log(this.value)
        }
    };

    foo.bar(); // 1
    // 模拟步骤
    // 1， 将函数设为对象的属性
    // 2， 执行该函数
    // 3， 删除该函数
    // 第一步
    // foo.fn = bar
    // 第二步
    // foo.fn()
    // 第三步
    // delete foo.fn
    // 根据这个思路，我们可以尝试着去写第一版的 **call2** 函数：
    //【3】模拟实现第一版
    Function.prototype.call2 = function(context) {
        // 首先要获取调用call的函数，用this可以获取
        context.fn = this;
        context.fn();
        delete context.fn;
    }

    // 测试一下
    var foo = {
        value: 1
    };

    function bar() {
        console.log(this.value);
    }

    bar.call2(foo); // 1
    // 【4】模拟实现第二版
    // 最一开始也讲了，call 函数还能给定参数执行函数。举个例子：
    var foo = {
        value: 1
    };

    function bar(name, age) {
        console.log(name)
        console.log(age)
        console.log(this.value);
    }

    bar.call(foo, 'kevin', 18);
    // kevin
    // 18
    // 1

    // 注意：传入的参数并不确定，这可咋办？
    // 不急，我们可以从 Arguments 对象中取值，取出第二个到最后一个参数，然后放到一个数组里。
    // 【因为第一个参数是调用这个函数的调用体，所有从第二个开始】
    // 比如这样：

    // 以上个例子为例，此时的arguments为：
    // arguments = {
    //      0: foo,
    //      1: 'kevin',
    //      2: 18,
    //      length: 3
    // }

    // 因为arguments是类数组对象，所以可以用for循环
    var args = [];
    for (var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]

    // 不定长的参数问题解决了，我们接着要把这个参数数组放到要执行的函数的参数里面去。

    // 将数组里的元素作为多个参数放进函数的形参里
    // context.fn(args.join(','))
    // (O_o)??
    // 这个方法肯定是不行的啦！！！
    // 也许有人想到用 ES6 的方法，不过 call 是 ES3 的方法，我们为了模拟实现一个 ES3 的方法，
    // 要用到ES6的方法，好像……，嗯，也可以啦。但是我们这次用 eval 方法拼成一个函数，类似于这样：

    eval('context.fn(' + args + ')');
    // 这里 args 会自动调用 Array.toString() 这个方法。
    // 所以我们的第二版克服了两个大问题，代码如下：
    // 第二版
    Function.prototype.call2 = function(context) {
        context.fn = this;
        var args = [];
        for (var i = 1, len = arguments.length; i < len; i++) {
            args.push('arguments[' + i + ']');
        }
        eval('context.fn(' + args + ')');
        delete context.fn;
    }

    // 测试一下
    var foo = {
        value: 1
    };

    function bar(name, age) {
        console.log(name)
        console.log(age)
        console.log(this.value);
    }

    bar.call2(foo, 'kevin', 18);
    // kevin
    // 18
    // 1
    // 【5】模拟实现第三版
    // 模拟代码已经完成 80%，还有两个小点要注意：
    // 1.this 参数可以传 null，当为 null 的时候，视为指向 window
    // 举个例子：
    var value = 1;

    function bar() {
        console.log(this.value);
    }
    bar.call(null); // 1
    // 虽然这个例子本身不使用 call，结果依然一样。
    // 2.函数是可以有返回值的！
    // 举个例子：
    var obj = {
        value: 1
    }

    function bar(name, age) {
        return {
            value: this.value,
            name: name,
            age: age
        }
    }
    console.log(bar.call(obj, 'kevin', 18));
    // Object {
    //    value: 1,
    //    name: 'kevin',
    //    age: 18
    // }
    // 不过都很好解决，让我们直接看第三版也就是最后一版的代码：
    // 第三版
    Function.prototype.call2 = function(context) {
            var context = context || window; //健壮代码，让其可以适用null时候指向window
            context.fn = this;
            var args = [];
            for (var i = 1, len = arguments.length; i < len; i++) {
                args.push('arguments[' + i + ']');
            }
            //此处用变量result将eval执行结果承接，然后返回。返回值的处理是这样的
            var result = eval('context.fn(' + args + ')');

            delete context.fn
            return result;
        }
        // 测试一下
    var value = 2;
    var obj = {
        value: 1
    }

    function bar(name, age) {
        console.log(this.value);
        return {
            value: this.value,
            name: name,
            age: age
        }
    }
    bar.call2(null); // 2
    console.log(bar.call2(obj, 'kevin', 18));
    // 1
    // Object {
    //    value: 1,
    //    name: 'kevin',
    //    age: 18
    // }
    // 到此，我们完成了 call 的模拟实现，给自己一个赞 ｂ（￣▽￣）ｄ
    //【5】apply的模拟实现
    // apply 的实现跟 call 类似，在这里直接给代码，代码来自于知乎 @郑航的实现：
    Function.prototype.apply = function(context, arr) {

        // var context = Object(context) || window; 这里有问题吗？
        // context为null时Object(null)返回空对象，不会被赋值为window
        // 非严格模式下，指定为 null 或 undefined 时会自动指向全局对象

        var context = Object(context) || window;
        context.fn = this;

        var result;
        if (!arr) {
            result = context.fn();
        } else {
            var args = [];
            for (var i = 0, len = arr.length; i < len; i++) {
                args.push('arr[' + i + ']');
            }
            result = eval('context.fn(' + args + ')')
        }

        delete context.fn
        return result;
    }
</script>

</html>

</html>